package cn.jingyuan.swan.cloud.mapping;

import cn.jingyuan.bee.utils.ReflectUtils;
import cn.jingyuan.bee.utils.StringUtils;
import io.swagger.annotations.ApiOperation;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.*;

/**
 * SpringBoot ApiResourceScanHandler 实现
 */
public class SpringBootApiResourceScanHandler extends AbstractApiResourceScanHandler implements ApplicationListener<ApplicationReadyEvent> {

    private final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    /** 忽略的资源 */
    protected final List<String> IGNORE_URLS = new ArrayList<>();

    public SpringBootApiResourceScanHandler() {
        super();
        IGNORE_URLS.add("/error");
    }

    @Override
    public Collection<String> getIgnoreScanUrls() {
        return IGNORE_URLS;
    }

    @Override
    public void addIgnoreScanUrls(Collection<String> urls) {
        if (urls != null && !urls.isEmpty()) {
            // FIXME 校验 urls 的有效性
            IGNORE_URLS.addAll(urls);
        }
    }

    /**
     * 初始化方法
     */
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        if (!scanState) {
            log.info("尚未启用 api 资源扫描");
            return;
        }

        ConfigurableApplicationContext context = event.getApplicationContext();
        Environment environment = context.getEnvironment();

        ApiScanPostProcessService scanPostProcessService = context.getBean(ApiScanPostProcessService.class);
        if (null == scanPostProcessService) {
            throw new RuntimeException("未发现" + ApiScanPostProcessService.class.getSimpleName() + " 服务");
        }

        // 服务名称
        if (null == serviceId) {
            serviceId = environment.getProperty("spring.application.name");
        }

        if (null == serviceId) {
            throw new IllegalArgumentException("serviceId cannot be empty");
        }

        // FIXME contextPath
        contextPath = environment.getProperty("xxx");

        // 所有接口映射
        RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);

        // 获取 url 与类和方法的对应信息
        Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();

        List<RequestMatcher> permitAll = new ArrayList<>();

        // FIXME 考虑柔性实现，不强制依赖 spring-security-xxx 模块
        try {
            // 获取所有安全配置适配器
            Map<String, WebSecurityConfigurerAdapter> securityConfigMap = context.getBeansOfType(WebSecurityConfigurerAdapter.class);
            Iterator<Map.Entry<String, WebSecurityConfigurerAdapter>> iterable = securityConfigMap.entrySet().iterator();

            while (iterable.hasNext()) {
                WebSecurityConfigurerAdapter configurer = iterable.next().getValue();
                HttpSecurity httpSecurity = (HttpSecurity) ReflectUtils.getFieldValue(configurer, "http");
                FilterSecurityInterceptor filterSecurityInterceptor = httpSecurity.getSharedObject(FilterSecurityInterceptor.class);
                FilterInvocationSecurityMetadataSource metadataSource = filterSecurityInterceptor.getSecurityMetadataSource();
                Map<RequestMatcher, Collection<ConfigAttribute>> requestMap = (Map) ReflectUtils.getFieldValue(metadataSource, "requestMap");

                Iterator<Map.Entry<RequestMatcher, Collection<ConfigAttribute>>> requestIterable = requestMap.entrySet().iterator();
                while (requestIterable.hasNext()) {
                    Map.Entry<RequestMatcher, Collection<ConfigAttribute>> match = requestIterable.next();
                    if (match.getValue().toString().contains("permitAll")) {
                        permitAll.add(match.getKey());
                    }
                }
            }
        } catch (Exception e) {
            log.error("扫描 request mapping 时发生错误", e);
        }

        List<ApiModel> apiModels = new ArrayList<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
            RequestMappingInfo mappingInfo = entry.getKey();
            HandlerMethod handlerMethod = entry.getValue();

            boolean ignore = MatchIgnoreApiUtils.isIgnore(handlerMethod);
            if (ignore) {
                continue;
            }

            // 请求路径
            PatternsRequestCondition patternsCondition = mappingInfo.getPatternsCondition();
            String urls = getUrls(patternsCondition.getPatterns());

            // 忽略指定的 urls
            // FIXME 需要优化判断逻辑，防止多个请求映射为在一个 method 上
            if (IGNORE_URLS.contains(urls)) {
                continue;
            }

            // FIXME 优先获取  RequestMapping produces 的属性
             /*
                判断 MediaType 是否为 Json
                    a)：Controller 是否被 RestController.class 修饰
                    b)：请求参数中包含 RequestBody.class
                    c)：方法是否被 ResponseBody.class 修饰
            */
            Set<MediaType> mediaTypeSet = mappingInfo.getProducesCondition().getProducibleMediaTypes();
            if (null != handlerMethod.getMethod().getDeclaringClass().getAnnotation(RestController.class)) {
                mediaTypeSet.add(MediaType.APPLICATION_JSON);
            } else if (null != handlerMethod.getMethod().getAnnotation(RequestBody.class)) {
                mediaTypeSet.add(MediaType.APPLICATION_JSON);
            } else {
                for (MethodParameter params : handlerMethod.getMethodParameters()) {
                    if (params.hasParameterAnnotation(RequestBody.class)) {
                        mediaTypeSet.add(MediaType.APPLICATION_JSON);
                        break;
                    }
                }
            }

            String mediaTypes = getMediaTypes(mediaTypeSet);

            // 请求类型
            RequestMethodsRequestCondition methodsCondition = mappingInfo.getMethodsCondition();
            String methods = getMethods(methodsCondition.getMethods());

            // 方法名 类名
            String className = handlerMethod.getMethod().getDeclaringClass().getName();
            String methodName = handlerMethod.getMethod().getName();

            // md5 码
            String md5 = DigestUtils.md5DigestAsHex((serviceId + urls).getBytes());

            String name = "";
            String desc = "";

            // 是否需要安全认证 默认：1-是 0-否
            String isAuth = "1";

            // 匹配项目中 .permitAll() 配置
            for (String url : patternsCondition.getPatterns()) {
                for (RequestMatcher requestMatcher : permitAll) {
                    if (requestMatcher instanceof AntPathRequestMatcher) {
                        AntPathRequestMatcher pathRequestMatcher = (AntPathRequestMatcher) requestMatcher;
                        if (PATH_MATCHER.match(pathRequestMatcher.getPattern(), url)) {
                            // 忽略验证
                            isAuth = "0";
                        }
                    }
                }
            }

            ApiOperation apiOperation = handlerMethod.getMethodAnnotation(ApiOperation.class);
            if (null != apiOperation) {
                name = apiOperation.value();
                desc = apiOperation.notes();
            }

            name = StringUtils.isBlank(name) ? methodName : name;

            ApiModel apiModel = new ApiModel()
                .setProject(serviceId)
                .setContextPath(contextPath)
                .setApiCode(md5)
                .setApiPath(urls)
                .setApiName(name)
                .setApiDesc(desc)
                .setHttpMethods(methods)
                .setContentTypes(mediaTypes)
                .setClassName(className)
                .setClassMethodName(methodName)
                .setMd5(md5)
                .setIsAuth(isAuth);

            apiModels.add(apiModel);
        }

        log.info("ApplicationReadyEvent:{}", serviceId);

        scanPostProcessService.scanPostProcess(apiModels);
    }

    private String getMediaTypes(Set<MediaType> mediaTypes) {
        StringBuilder builder = new StringBuilder();
        for (MediaType mediaType : mediaTypes) {
            builder.append(mediaType.toString()).append(",");
        }

        if (mediaTypes.size() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }

        return builder.toString();
    }

    private String getMethods(Set<RequestMethod> requestMethods) {
        StringBuilder builder = new StringBuilder();
        for (RequestMethod requestMethod : requestMethods) {
            builder.append(requestMethod.toString()).append(",");
        }

        if (requestMethods.size() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }

        return builder.toString();
    }

    private String getUrls(Set<String> urls) {
        StringBuilder builder = new StringBuilder();
        for (String url : urls) {
            builder.append(url).append(",");
        }

        if (urls.size() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }

        return builder.toString();
    }

}
